import os
import time
import typing
from typing import Tuple, Literal

import allure
from datetime import datetime
from playwright.sync_api import Page, Locator, Position

from sample.common.read_config import conf
from config.sys_path import PASS_IMG_DIR
from config.sys_path import FAIL_IMG_DIR
from sample.common.my_log import my_log
from sample.util.pytest_util import PytestUtil
from sample.common.element_locators import el


class BasePage:
    URL = None

    def __init__(self, page: Page):
        self.page = page

    def __init_subclass__(cls, **kwargs):
        """初始化子类的URL属性，代替playwright的原生base_url设置方式"""
        super().__init_subclass__(**kwargs)
        folder = os.path.abspath(cls.__module__).replace(".", os.sep).split(os.sep)
        try:
            index = folder.index("page_object") + 1
            directory = folder[index]
            cls.URL = conf.get('base_url', directory) + cls.URL
        except ValueError:
            print('子类不在页面对象类目录下')

    def navigate(self, url):
        """打开指定的Url"""
        try:
            self.page.goto(url)
            self.page.wait_for_load_state()  # 等待页面资源加载完毕，如js,img等。
            my_log.info(f'使用地址【{url}】，成功进入页面--【{self.page.url}】')
        except Exception as e:
            my_log.error(f'打开页面失败--【{url}】')
            my_log.exception(e)
            self.__error_screenshot()
            raise

    def get_locator(self, selector: str, get_last=False):
        """
        获取元素定位器，存在多个元素时，默认取第一个。
        :param selector: 请输入ini文件中的元素名称，格式为section.option,placeholder 。
        也可直接输入元素选择器,如XPATH / CSS等
        :param get_last: 是否取最后一个元素
        """
        selector = el.get_el_and_format(selector)
        locator = self.page.locator(selector)
        if get_last:
            locator = locator.last
        else:
            locator = locator.first
        my_log.info(f'获取元素定位器--【{locator}】')
        locator.highlight()  # 元素高亮，推荐仅在调试时使用，自动化脚本完成后，建议关闭此功能。
        return locator

    def get_locators(self, selector: str):
        """
        获取元素定位器列表，仅在确定有多个元素时使用。
        :param selector: 请输入ini文件中的元素名称，格式为section.option,placeholder 。
        也可直接输入元素选择器,如XPATH / CSS等
        """
        selector = el.get_el_and_format(selector)
        try:
            locators = self.page.locator(selector)
            locators.nth(0).wait_for(timeout=2000, state='attached')
            if locators.count():
                my_log.info(f'成功定位到【{locators.count()}】个元素--【{locators}】')
            else:
                raise Exception(f'locators.count()={locators.count()}')
        except Exception as e:
            my_log.error(f'元素定位失败--【{selector}】')
            my_log.exception(e)
            self.__error_screenshot()
            raise
        return locators.all()

    def click_element(self, selector, *,
                      modifiers: typing.Optional[
                          typing.List[Literal["Alt", "Control", "Meta", "Shift"]]
                      ] = None,
                      position: typing.Optional[Position] = None,
                      button: typing.Optional[Literal["left", "middle", "right"]] = None,
                      force: bool = False):
        """
        点击元素
        :param selector: 请输入ini文件中的元素名称，格式为section.option,placeholder 。
        也可直接输入元素选择器,如XPATH / CSS等
        :param modifiers: 联合点击，比如按住Alt+Control后点击鼠标左键。
        :param position: 点击元素指定坐标位置，如点击元素左上角：position={'x': 0, 'y': 0}。
        :param button: 点击鼠标的哪个健，默认左键。
        :param force: 是否启用强制点击，默认否。当点击元素失败时，可尝试开启。
        """
        locator = self.get_locator(selector)
        try:
            locator.click(modifiers=modifiers, position=position, button=button, force=force)
            my_log.info(f'点击元素成功--【{locator}】')
        except Exception as e:
            my_log.error(f'点击元素失败--【{locator}】')
            my_log.exception(e)
            self.__error_screenshot()
            raise

    def dblclick_element(self, selector, *,
                         modifiers: typing.Optional[
                             typing.List[Literal["Alt", "Control", "Meta", "Shift"]]
                         ] = None,
                         position: typing.Optional[Position] = None):
        """
        鼠标双击元素
        :param selector: 请输入ini文件中的元素名称，格式为section.option,placeholder 。
        也可直接输入元素选择器,如XPATH / CSS等
        :param modifiers: 联合点击，比如按住Alt+Control后点击鼠标左键。
        :param position: 点击元素指定坐标位置，如点击元素左上角：position={'x': 0, 'y': 0}。
        """
        locator = self.get_locator(selector)
        try:
            locator.dblclick(modifiers=modifiers, position=position)
            my_log.info(f'双击元素成功--【{locator}】')
        except Exception as e:
            my_log.error(f'双击元素失败--【{locator}】')
            my_log.exception(e)
            self.__error_screenshot()
            raise

    def fill_input(self, selector, value):
        """
        输入框输入
        :param value:输入的值
        :param selector: 请输入ini文件中的元素名称，格式为section.option,placeholder 。
        也可直接输入元素选择器,如XPATH / CSS等
        """
        locator = self.get_locator(selector)
        text = str(value)
        try:
            locator.fill(text)
            my_log.info(f'输入成功--【{locator}】--输入内容【{text}】')
        except Exception as e:
            my_log.error(f'输入失败--【{locator}】--待输入【{text}】')
            my_log.exception(e)
            self.__error_screenshot()
            raise

    def clear_input(self, selector):
        """
        清空输入框
        :param selector: 请输入ini文件中的元素名称，格式为section.option,placeholder 。
        也可直接输入元素选择器,如XPATH / CSS等
        :return:
        """
        locator = self.get_locator(selector)
        try:
            locator.clear()
            my_log.info(f'清空输入框成功--【{locator}】')
        except Exception as e:
            my_log.error(f'清空输入框失败--【{locator}】')
            my_log.exception(e)
            self.__error_screenshot()
            raise

    def set_checked(self, selector, checked: bool):
        """
        设置单选按钮或复选框的选中状态（符合预期状态则不操作）
        :param selector: 单选按钮或复选框的请输入ini文件中的元素名称，格式为section.option,placeholder 。
        也可直接输入元素选择器,如XPATH / CSS等
        :param checked: 是否选中，True/False
        """
        locator = self.get_locator(selector)
        try:
            locator.set_checked(checked, force=True)
            if checked:
                my_log.info(f'元素选中成功--【{locator}】')
            else:
                my_log.info(f'元素取消选中成功--【{locator}】')
        except Exception as e:
            if checked:
                my_log.info(f'元素选中失败--【{locator}】')
            else:
                my_log.info(f'元素取消选中失败--【{locator}】')
            my_log.exception(e)
            self.__error_screenshot()
            raise

    def get_text(self, selector):
        """
        获取元素的文本内容
        :param selector: 请输入ini文件中的元素名称，格式为section.option,placeholder 。
        也可直接输入元素选择器,如XPATH / CSS等
        :return:
        """
        locator = self.get_locator(selector)
        try:
            text = locator.text_content()
            my_log.info(f'获取文本内容成功--【{locator}】--【text:{text}】')
        except Exception as e:
            my_log.error(f'获取文本内容失败--【{locator}】')
            my_log.exception(e)
            self.__error_screenshot()
            raise
        return text

    def get_value(self, selector):
        locator = self.get_locator(selector)
        try:
            value = locator.input_value()
            my_log.info(f'获取value值成功--【{locator}】--value:【{value}】')
        except Exception as e:
            my_log.error(f'获取value值失败--【{locator}】')
            my_log.exception(e)
            self.__error_screenshot()
            raise
        return value

    def get_attribute(self, selector, attribute_name):
        """
        获取元素属性
        :param attribute_name: 属性名
        :param selector: 元素选择器
        :return:
        """
        locator = self.get_locator(selector)
        try:
            res = locator.get_attribute(attribute_name)
            my_log.info(f'获取[{attribute_name}]属性成功--【{res}】')
        except Exception as e:
            my_log.error(f'获取[{attribute_name}]属性失败--【{locator}】')
            my_log.exception(e)
            self.__error_screenshot()
            raise
        return res

    def wait_for_timeout(self, timeout: float):
        """
        强制等待方法（由于playwright底层是异步代码，使用time.sleep()可能会导致意外的错误）
        :param timeout: 等待时长，毫秒
        """
        self.page.wait_for_timeout(timeout)

    def wait_for_element(self, selector: str, *, timeout=30000,
                         state: typing.Optional[
                             Literal["attached", "detached", "hidden", "visible"]
                         ] = 'visible',
                         ):
        """等待元素某种状态
        注意：playwright的元素操作有自动等待，因此大多数情况下不需要再使用此方法进行等待。
        :param selector : 元素选择器
        :param timeout : 超时时间，默认30秒
        :param state : 可选项 "attached", "detached", "hidden", "visible" , Defaults to `'visible'`.
        """
        locator = self.get_locator(selector)
        try:
            start = time.time()
            locator.wait_for(timeout=timeout, state=state)
            end = time.time()
            my_log.info(f'成功等待元素出现，用时{round(end - start, 2)}秒--【{locator}】')
        except Exception as e:
            my_log.error(f'等待元素已超时--【{locator}】')
            my_log.exception(e)
            self.__error_screenshot()
            raise
        return locator

    def is_element_exist(self, selector: str) -> bool:
        """判断元素是否存在"""
        try:
            self.get_locator(selector).wait_for(timeout=2000)
        except Exception:
            return False
        return True

    def get_page_after_click(self, selector: str):
        """返回点击元素后，新打开的页面的page对象
        :param selector: 要点击的元素的定位器
        """
        try:
            with self.page.context.expect_page() as new_page_info:
                self.click_element(selector)
            new_page = new_page_info.value
            new_page.wait_for_load_state()
        except Exception as e:
            my_log.error(f'点击元素并打开新页面失败--【{selector}】')
            my_log.exception(e)
            self.__error_screenshot()
            raise
        return new_page

    def select_option(self, selector, *value):
        """
        前端下拉框组件为ul-li元素时，可此方法选择需要的选项
        """
        try:
            self.click_element(selector)
            for i in value:
                option = f'(//ul/li/span[text()="{i}"])[last()]'
                self.click_element(option)
        except Exception as e:
            my_log.error(f'选择ul下拉框中li选项失败--【{selector}】--【{value}】')
            my_log.exception(e)
            self.__error_screenshot()
            raise

    def save_screenshot_allure(self, name='操作截图'):
        """
        保存截图并添加到allure报告
        :param name: allure报告截图标题名称
        :return:
        """
        if name == '失败截图':
            path = FAIL_IMG_DIR
        else:
            path = PASS_IMG_DIR
        i_data = time.strftime('%Y-%m-%d')
        test_case_name = PytestUtil.get_test_case_name()
        path = os.path.join(path, i_data, test_case_name)
        if not os.path.exists(path):
            os.makedirs(path)
        i_time = datetime.now().strftime('%H-%M-%S-%f')[:-3]
        filename = i_time + '.png'
        file_path = os.path.join(path, filename)
        try:
            self.page.screenshot(path=file_path, caret='initial')
        except Exception as e:
            my_log.error('屏幕截图失败')
            my_log.exception(e)
            raise
        else:
            with open(file_path, 'rb') as f:
                content = f.read()
                allure.attach(content, name, allure.attachment_type.PNG)
            my_log.info(f'{name},图片路径：【{file_path}】')

    def screenshot(self):
        """        操作截图        """
        self.save_screenshot_allure()

    def __error_screenshot(self):
        """        失败截图        """
        self.save_screenshot_allure('失败截图')

    def click_element_js(self, selector: str):
        """通过JS点击页面上的元素"""
        locator = self.get_locator(selector)
        locator.wait_for(state='attached')
        self.page.wait_for_load_state('networkidle')
        try:
            locator.evaluate('el => el.click()')
            my_log.info(f"js点击元素成功--【{locator}】")
        except Exception as e:
            my_log.error(f"js点击元素失败--【{locator}】")
            my_log.exception(e)
            self.__error_screenshot()
            raise

    def hover_element(self, selector: str):
        """
        鼠标移动到某个元素
        :param selector: 请输入ini文件中的元素名称，格式为section.option,placeholder 。
        也可直接输入元素选择器,如XPATH / CSS等
        """
        locator = self.get_locator(selector)
        try:
            locator.hover()
            my_log.info(f'鼠标移动到元素并悬停成功--【{locator}】')
        except Exception as e:
            my_log.error(f'鼠标移动到元素并悬停失败--【{locator}】')
            my_log.exception(e)
            self.__error_screenshot()
            raise

    def drag_to_element(self, selector: str, target_selector: str):
        """
        将元素拖动至另一个元素
        :param selector: 要拖动的元素定位器
        :param target_selector: 目标元素定位器
        """
        el = self.get_locator(selector)
        target_el = self.get_locator(target_selector)
        try:
            el.drag_to(target_el)
            my_log.info(f'拖动元素成功--【{selector}】>>【{target_selector}】')
        except Exception as e:
            my_log.error(f'拖动元素失败--【{selector}】>>【{target_selector}】')
            my_log.exception(e)
            self.__error_screenshot()
            raise

    def get_element_size(self, selector: str) -> Tuple[float, float]:
        """获取元素的宽高"""
        locator = self.get_locator(selector)
        try:
            x = locator.bounding_box()['width']
            y = locator.bounding_box()['height']
            my_log.info(f'获取元素宽高成功--【x={x},y={y}】--【{locator}】')
        except Exception as e:
            my_log.error(f'获取元素宽高失败--【{locator}】')
            my_log.exception(e)
            self.__error_screenshot()
            raise
        return x, y

    def show_page(self):
        """
        将调用此方法的标签页page展示在屏幕上（类似在浏览器上点击标签页）
        """
        self.page.bring_to_front()

    # 以下iframe相关方法，未经使用，尚无法确定功能是否完善。
    # 后续有使用场景时，需多做尝试以完善方法。
    def get_all_iframe(self):
        """返回当前页面所有的iframe"""
        return self.page.frames

    def get_iframe(self, selector: str, get_last=False):
        """
        获取 iframe，以便于对其内部元素进行操作
        :param selector: 请输入ini文件中的元素名称，格式为section.option,placeholder 。
        也可直接输入元素选择器,如XPATH / CSS等
        :param get_last: 存在多个iframe时，是否取最后一个
        """
        selector = el.get_el_and_format(selector)
        try:
            iframe = self.page.frame_locator(selector)
            my_log.info(f'获取iframe成功--【{selector}】')
        except Exception as e:
            my_log.error(f'获取iframe失败--【{selector}】')
            my_log.exception(e)
            self.__error_screenshot()
            raise
        if get_last:
            return iframe.last
        else:
            return iframe.first

    def get_element_in_iframe(self, iframe_selector: str, element_selector: str, get_last=False):
        """
        获取iframe内部的元素集合，此方法不推荐使用。
        可通过playwright独有的链式元素定位方式(>>)实现同样效果。
        例如：page.get_element('//iframe[@name="my_iframe"] >> //div/span')

        :param iframe_selector: iframe元素选择器，请输入ini文件中的元素名称，格式为section.option,placeholder 。
        也可直接输入元素选择器,如XPATH / CSS等
        :param element_selector: element元素选择器
        :param get_last: 存在多个元素时，是否取最后一个
        """
        iframe_selector = el.get_el_and_format(iframe_selector)
        element_selector = el.get_el_and_format(element_selector)
        loc_list = self.get_elements_in_iframe(iframe_selector, element_selector)
        if get_last:
            return loc_list[-1]
        else:
            return loc_list[0]

    def get_elements_in_iframe(self, iframe_selector: str, element_selector: str) -> list[Locator]:
        """获取iframe内部的元素集合，此方法不推荐使用。
        可通过playwright独有的链式元素定位方式(>>)实现同样效果。
        例如：page.get_locator('//iframe[@name="my_iframe"] >> //div/span')
        :param iframe_selector: iframe元素选择器，请输入ini文件中的元素名称，格式为section.option,placeholder 。
        也可直接输入元素选择器,如XPATH / CSS等
        :param element_selector: element元素选择器
        """
        iframe_selector = el.get_el_and_format(iframe_selector)
        element_selector = el.get_el_and_format(element_selector)
        try:
            iframe = self.page.frame_locator(iframe_selector).first
            elements = iframe.locator(element_selector)
            if elements.count():
                my_log.info(f'从iframe内部成功定位到【{elements.count()}】个元素--【{element_selector}】')
            else:
                raise Exception(f'从iframe内部根据【{element_selector}】未定位到任何元素')
        except Exception as e:
            my_log.error(f'从iframe内部获取元素失败--【{iframe_selector}】>>【{element_selector}】')
            my_log.exception(e)
            self.__error_screenshot()
            raise
        return elements.all()

    # 以上iframe相关方法，未经使用，尚无法确定功能是否完善。
    # 后续有使用场景时，需多做尝试以完善方法。

    def test__(self):
        """用于测试page原生方法，可忽略"""
        self.page.on("request")
        self.page.expect_popup()
        self.page.reload()

        pass

    def button_按钮(self, text, nth=-1):
        button = self.page.locator("button")
        for word in text:
            button = button.filter(has_text=word)
        if button.count() > 1 and nth == -1:
            min_len = 0
            min_bt = None
            for bt in button.all():
                l = len(bt.text_content())
                if min_len >= l:
                    min_len = l
                    min_bt = bt
            return min_bt
        return button.locator('visible=true').nth(nth)
